使用Go实现依赖注入

在第7章介绍的简单Web服务中, handleRequest 处理器函数会将 GET 请求转发给 handleGet 函数,后者会从URL中提取文章的ID,然后通过 data.go文件 中的 retrieve 函数获取与文章ID相对应的 Post 结构。当 retrieve 函数被调用时,它会使用全局的 sql.DB 结构去打开一个连接至PostgreSQL的数据库连接,并在 posts 表中查找指定的数据。

图8-3展示了简单Web服务在处理 GET 请求时的函数调用流程。除 retrieve 函数需要通过全局的 sql.DB 实例访问数据库之外,访问数据库对于其他函数来说都是透明的(transparent)。

50.png

图8-3 简单Web服务在处理 `GET` 请求时的函数调用流程图

正如图8-3所示, handleRequesthandleGet 都依赖于 retrieve 函数,而后者最终又依赖于 sql.DB 。因为对 sql.DB 的依赖是整个问题的根源,所以我们必须将其移除。

跟很多问题一样,解耦依赖关系也存在着好几种不同的方式:既可以从底部开始,对数据抽象层的依赖关系进行解耦,然后直接获取 sql.DB 结构,也可以从顶部开始,将 sql.DB 注入到 handleRequest 当中。本节要介绍的是后一种方法,也就是以自顶向下的方式解耦依赖关系的方法。

图8-4展示了移除对 sql.DB 的依赖并将这种依赖通过主程序注入函数调用流程中的具体方法。注意,问题的关键并不是避免使用 sql.DB ,而是避免对它的直接依赖,这样我们才能够在测试时使用测试替身。

51.png

图8-4 将一个包含 `sql.DB` 的 `Post` 结构传递到函数调用流程中,以此来对简单Web服务实现依赖注入模式。因为 `Post` 结构已经包含了 `sql.DB` ,所以调用流程中的所有函数都不再依赖 `sql.DB`

正如前面所说,为了解耦被调用函数对 sql.DB 的依赖,我们可以将 sql.DB 注入 handleRequest ,但是把 sql.DB 实例或者指向 sql.DB的 指针作为参数传递给 handleRequest 对解决问题是没有任何帮助的,因为这样做只不过是将问题推给了控制流的上游。作为替代,我们需要将代码清单8-12所示的 Text 接口传递给 handleRequest 。当测试程序需要从数据库里面获取一篇文章时,它可以调用 Text 接口的方法,并假设这个方法知道自己应该做什么以及应该返回什么数据。

代码清单8-12 传递给 handleRequest 的接口

type Text interface {
 fetch(id int) (err error)
 create() (err error)
 update() (err error)
 delete() (err error)
}

接下来,我们要做的就是让 Post 结构实现 Text 接口,并将它的一个字段设置成一个指向 sql.DB 的指针。为了让 Post 结构实现 Text 接口,我们需要让 Post 结构实现 Text 接口拥有的所有方法,不过由于代码清单8-12中定义的 Text 接口原本就是根据 Post 结构拥有的方法定义而来的,所以 Post 结构实际上已经实现了 Text 接口。代码清单8-13展示了添加新字段之后的 Post 结构。

代码清单8-13 添加了新字段之后的 Post 结构

type Post struct {
 Db   *sql.DB
 Id   int `json:"id"`
 Content string `json:"content"`
 Author string `json:"author"`
}

这种做法解决了将 sql.DB 直接传递给 handleRequest 的问题:程序并不需要将 sql.DB 传递给被调用的函数,它只需要和之前一样,向被调用的函数传递 Post 结构的实例即可,而 Post 结构的各个方法也会使用结构内部的 sql.DB 指针来代替原本对全局变量的访问。因为 handleRequest 函数还是和以前一样,接受 Post 结构作为参数,所以它的签名不需要做任何修改。在根据新的 Post 结构做相应的修改之后, handleRequest 函数如代码清单8-14所示。

代码清单8-14 修改后的 handleRequest 函数

func handleRequest(t Text) http.HandlerFunc { ❶
  return func(w http.ResponseWriter, r *http.Request) { ❷
  var err error
  switch r.Method {
  case "GET":
   err = handleGet(w, r, t) ❸
  case "POST":
   err = handlePost(w, r, t)
  case "PUT":
   err = handlePut(w, r, t)
  case "DELETE":
   err = handleDelete(w, r, t)
  }
  if err != nil {
   http.Error(w, err.Error(), http.StatusInternalServerError)
   return
  }
 }
}

❶ 传入Text 接口

❷ 返回带有正确签名的函数

❸ 将Text接口传递给实际的处理器

正如代码所示,因为 handleRequest 函数已经不再遵循 ServeHTTP 方法的签名规则,所以它已经不再是一个处理器函数了。这使我们无法再使用 HandleFunc 函数把它与一个URL绑定在一起了。

为了解决这个问题,程序再次使用了本书第3章中介绍过的处理器串联技术,让 handleRequest 返回了一个 http.HandlerFunc 函数。

之后,程序在 main 函数里面将不再绑定 handleRequest 函数到URL,而是直接调用 handleRequest 函数,让它返回一个 http.HandleFunc 函数。因为被返回的函数符合 HandleFunc 方法的签名要求,所以程序就可以像之前一样,把它用作处理器并与指定的URL进行绑定。代码清单8-15展示了修改后的 main 函数。

代码清单8-15 修改后的 main 函数

func main() {
 var err error
 db, err := sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=
  disable")
 if err != nil {
  panic(err)
 }
 server := http.Server{
  Addr: ":8080",
 }
 http.HandleFunc("/post/", handleRequest(&Post{Db: db})) ❶
 server.ListenAndServe()
}

❶ 将Post 结构传递给handleRequest函数,然后绑定函数返回的处理器

注意,程序通过 Post 结构,以间接的方式将指向 sql.DB 的指针传递给了 handleRequest 函数,这就是将依赖关系注入 handleRequest 的方法。代码清单8-16展示了同样的依赖关系是如何被注入 handleGet 函数的。

代码清单8-16 修改后的 handleGet 函数

func handleGet(w http.ResponseWriter, r *http.Request, post Text) (err error) { ❶
 id, err := strconv.Atoi(path.Base(r.URL.Path))
 if err != nil {
  return
 }
 err = post.fetch(id) ❷
 if err != nil {
  return
 }
 output, err := json.MarshalIndent(post, "", "\t\t")
 if err != nil {
  return
 }
 w.Header().Set("Content-Type", "application/json")
 w.Write(output)
 return
}

❶ 接受Text 接口作为参数

❷ 获取数据并将其存储到Post 结构

修改后的 handleGet 函数跟之前差不多,主要区别在于现在的 handleGet 函数将直接接受 Post 结构,而不是像以前那样在内部创建 Post 结构。除此之外, handleGet 函数现在会通过调用 Post 结构的 fetch 方法来获取数据,而不必再调用需要访问全局 sql.DB 实例的 retrieve 函数。代码清单8-17展示了 Post 结构的 fetch 方法的具体定义。

代码清单8-17 新的 fetch 方法

func (post *Post) fetch(id int) (err error) {
 err = post.Db.QueryRow("select id, content, author from posts where id =
➥$1", id).Scan(&post.Id, &post.Content, &post.Author)
 return
}

这个 fetch 方法在访问数据库时不需要使用全局的 sql.DB 结构,而是使用被传入的 Post 结构的 Db 字段来访问数据库。如果我们现在编译并运行修改后的简单Web服务,那么它将和修改之前的简单Web服务一样正常工作。不同的地方在于,修改后的代码已经移除了对全局的 sql.DB 结构的依赖。

只要对数据库的依赖还深埋在代码之中,我们就无法对其进行独立的测试。为此,我们在上面花了不少功夫来移除代码中的依赖,从而使单元测试用例可以变得更为独立。在通过外部代码实现依赖注入之后,我们接下来就可以使用测试替身对程序进行测试了。

因为 handleRequest 函数能够接受任何实现了 Text 接口的结构,所以我们可以创建出一个实现了 Text 接口的测试替身,并把它作为传递给 handleRequest 函数的参数。代码清单8-18展示了一个名为 FakePost 的测试替身,以及它为了满足 Text 接口的要求而实现的几个方法。

代码清单8-18  FakePost 测试替身

package main
type FakePost struct {
 Id   int
 Content string
 Author string
}
func (post *FakePost) fetch(id int) (err error) {
 post.Id = id
 return
}
func (post *FakePost) create() (err error) {
 return
}
func (post *FakePost) update() (err error) {
 return
}
func (post *FakePost) delete() (err error) {
 return
}

注意,为了进行测试, fetch 方法会把所有传递给它的ID都设置为 FakePost 结构的ID。此外,虽然 FakePost 结构的其他方法在测试时都不会用到,但是为了满足 Text 接口的实现要求,程序还是为每个方法都定义了一个没有任何实际用途的空方法。为了保持代码的清晰,这些测试替身代码被放到了 doubles.go文件 里面。

接下来,我们还需要在 server_test.go文件 里为 handleGet 函数加上代码清单8-19所示的测试用例。

代码清单8-19 将测试替身依赖注入到 handleRequest

func TestHandleGet(t *testing.T) {
 mux := http.NewServeMux()
    mux.HandleFunc("/post/", handleRequest(&FakePost{}))  ❶
 writer := httptest.NewRecorder()
 request, _ := http.NewRequest("GET", "/post/1", nil)
 mux.ServeHTTP(writer, request)
 if writer.Code != 200 {
  t.Errorf("Response code is %v", writer.Code)
 }
 var post Post
 json.Unmarshal(writer.Body.Bytes(), &post)
 if post.Id != 1 {
  t.Errorf("Cannot retrieve JSON post")
 }
}

❶ 传入一个FakePost 结构来代替Post 结构

测试用例现在不再向 handleRequest 传递 Post 结构,而是传递一个 FakePost 结构,这个结构就是 handleRequest 所需的一切。除此之外,这个测试用例跟之前的测试用例没有什么不同。

为了验证测试替身是否能正常工作,我们可以在关闭数据库之后再次运行测试用例。在这种情况下,旧的测试用例将会因为无法连接数据库而失败,而使用了测试替身的测试用例则因为不需要实际的数据库而一切如常进行。这也意味着我们在辛苦了这么久之后,终于可以独立地测试 handleGet 函数了。

跟之前的测试一样,如果 handleGet 函数运作正常,那么测试就会通过;否则,测试就会失败。需要注意的是,这个测试用例并没有实际测试 Post 结构的 fetch 方法,这是因为实施这种测试需要对 posts 表执行预设操作和拆卸操作,而重复执行这种操作会在测试时耗费大量的时间。这样做的另一个好处是隔离了Web服务的各个部分,使程序员可以独立测试每个部分,并在发现问题时更准确地定位出错的部分。因为代码总是在不断地演进和变化当中,所以能够做到这一点是非常重要的。在代码不断衍化的过程中,我们必须保证后续添加的部分不会对前面已有的部分造成破坏。

results matching ""

    No results matching ""